using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Security.Principal;
using System.Security;
using System.Data.Common;
using System.Threading;
using System.Configuration;
using NBusiness.Utilities;
using NBusiness.Data.Validation;
using System.ComponentModel;
using NBusiness.Properties;
using System.Collections.ObjectModel;
using System.Globalization;

namespace NBusiness.Data
{
    [Serializable]
    public abstract class EntityBase : IHasChildren, INotifyPropertyChanged
    {
        #region Constructors
        /// <summary>
        /// Initializes base class and fetches Authorization Rules. 
        /// </summary>
        public EntityBase()
        {
            InitializeRelationshipEvents();
            InitializeActions();
            Initialize();
        }
        internal protected EntityBase(CriteriaBase criteria) : this()
        {
            string connectionStringName = (string)Settings.Instance.GetSetting(SettingsType.ConnectionStringName, this.GetType());
            string connstr = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
            using (DbConnection cn = ((IDatabaseProvider)Settings.Instance.GetSetting(SettingsType.Provider, this.GetType())).GetConnection(connstr))
            {
                cn.Open();
                Fetch(cn, criteria);
            }
        }
        protected internal EntityBase(DbConnection cn, CriteriaBase criteria) : this()
        {
            Fetch(cn, criteria);
        }
        internal protected EntityBase(DataReader dr, CriteriaBase criteria) : this()
        {
            Load(this, new LoadEventArgs(dr, criteria));
        }
        #endregion

        #region Initialize
        /// <summary>
        /// Implement in partial class to allow user code to attach events and implement custom code.
        /// </summary>
        public virtual void Initialize()
        {
        }
        #endregion

        #region Relationships
        /// <summary>
        /// Implement this method to assign event handlers for relationships.
        /// </summary>
        protected abstract void InitializeRelationshipEvents();
        #endregion

        #region Status Properties
        private bool _isNew = true;
        /// <summary>
        /// If true IsNew indicates that this object has not yet been inserted
        /// into the database.
        /// </summary>
        [Bindable(false)]
        public bool IsNew
        {
            get { return _isNew; }
        }
        public void MarkOld()
        {
            _isNew = false;
        }
        private bool _isDirty;
        /// <summary>
        /// If true IsDirty indicates that some property has been altered and
        /// this object needs to be saved to persist those changes.
        /// </summary>
        [Bindable(false)]
        public bool IsDirty
        {
            get { return _isDirty; }
        }
        /// <summary>
        /// Call this in user properties when a value is changed to indicated
        /// that it needs to be updated in the database when a Save is called.
        /// </summary>
        protected void MarkDirty()
        {
            _isDirty = true;
        }
        private bool _isDeleted;
        /// <summary>
        /// If true IsDeleted indicates that this object has been flagged for
        /// delete and should be Saved to be deleted from the database.
        /// </summary>
        [Bindable(false)]
        public bool IsDeleted
        {
            get { return _isDeleted; }
        }
        /// <summary>
        /// An entity may be marked as read only to prevent Persisting changes.
        /// </summary>
        [Bindable(false)]
        protected virtual bool IsReadOnly
        {
            get { return false; }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
        #endregion

		#region Save / Persist
		/// <summary>
		/// Call save on the object to persist its current state and all children
		/// in the database.
		/// </summary>
		public virtual void Save()
		{
			if (!IsReadOnly)
			{
                string connectionStringName = (string)Settings.Instance.GetSetting(SettingsType.ConnectionStringName, this.GetType());
                string connstr = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
                using (DbConnection cn = ((IDatabaseProvider)Settings.Instance.GetSetting(SettingsType.Provider, this.GetType())).GetConnection(connstr))
				{
					cn.Open();
					using (DbTransaction trans = cn.BeginTransaction())
					{
						try
						{
                            // Do the persistence
							Persist(trans);
						}
						catch(Exception innerException)
						{
                            try
                            {
                                // Undo all persistence calls.
                                trans.Rollback();

                                // Call the error event to allow for automatic logging.
                                OnError(innerException);
                            }
                            catch ( Exception outerException )
                            {
                                // if an exception is caught then something in the OnError message when wrong.
                                // Throw a new Exception to throw up both the original error and the new one from the error handler!
                                throw new EntityErrorException(Resources.EntityErrorMessage, outerException, innerException);
                            }

                            // the error handler was fine so throw up the
                            // original exception.
                            throw innerException;
						}

                        //No errors detected. Commit persist transaction.
						trans.Commit();
					}
				}
			}
		}

        [NonSerialized]
        private PersistenceGuard _persistenceGuard;
        /// <summary>
        /// Persists current state of object with the database. Makes calls Persisted
        /// and DeleteChildren where appropriate.
        /// </summary>
        /// <param name="transaction"></param>
        public virtual void Persist(DbTransaction transaction)
        {
            Persist(transaction, PersistType.Downward);
        }
		/// <summary>
		/// Persists current state of object with the database. Makes calls Persisted
		/// and DeleteChildren where appropriate.
		/// </summary>
		/// <param name="cn"></param>
        public virtual void Persist(DbTransaction trans, PersistType type)
		{
			if (!IsReadOnly && (_persistenceGuard == null || !_persistenceGuard.Persisting))
			{
                // The persistence guard prevents an object from being persisted more than once by
                // its relationships. It is done this way so that if there is an exception the
                // persistence guard will be turned off when Dispose is called. Otherwise an object
                // would no longer be able to persist for its lifetime.
                using (_persistenceGuard = new PersistenceGuard())
                {
                    //Always persist upwards
                    OnPersisting(trans);
                    if (IsNew && !IsDeleted)
                    {
                        if (!CanInsert()) throw new SecurityException(Resources.PersistNotAuthorizedToInsert);
                        if (!IsValid) throw new ValidationException(this, BrokenRules, Resources.PersistValidationExceptionMessage);

                        OnInserting(trans);
                        InsertEntity(trans);
                        _isNew = false;         //this must be set before OnPersisted so children don't try to insert a second time.
                        _isDirty = false;
                        OnInserted(trans);
                        
                    }
                    else if (IsDeleted)
                    {
                        if (!CanDelete()) throw new SecurityException(Resources.PersistNotAuthorizedToDelete);

                        OnDeleting(trans);
                        DeleteEntity(trans);
                        OnDeleted(trans);
                        _isNew = true;
                    }
                    else if (IsDirty)
                    {
                        if (!CanUpdate()) throw new SecurityException(Resources.PersistNotAuthorizedToUpdate);
                        if (!IsValid) throw new ValidationException(this, BrokenRules, Resources.PersistValidationExceptionMessage);
                        OnUpdating(trans);
                        UpdateEntity(trans);
                        OnUpdated(trans);
                        _isDirty = false;
                    }
                    //Only persist downwards as appropriate.
                    if(type == PersistType.Downward) OnPersisted(trans);
                }
			}
		}
        public event EventHandler<PersistEventArgs> Persisting;
		/// <summary>
		/// Call persist on all Parents.
		/// </summary>
		/// <param name="cn"></param>
		protected virtual void OnPersisting(DbTransaction transaction)
		{
			if (Persisting != null) Persisting(this, new PersistEventArgs(transaction, PersistType.Upward));
		}
        /// <summary>
        /// Persiste all children in the handler of this event.
        /// </summary>
        public event EventHandler<PersistEventArgs> Persisted;
		/// <summary>
		/// Call persist on all Children.
		/// </summary>
		/// <param name="cn"></param>
		protected virtual void OnPersisted(DbTransaction transaction)
		{
			if (Persisted != null) Persisted(this, new PersistEventArgs(transaction, PersistType.Downward));
		}
		#endregion

        #region Insert
        /// <summary>
        /// Cannot call Insert directly, insert can only be done as a result of a call to Save();
        /// </summary>
        /// <param name="cn"></param>
        protected abstract void InsertEntity(DbTransaction cn);

        public event EventHandler<InsertEventArgs> Inserting;
        protected virtual void OnInserting(DbTransaction transaction)
        {
            if (Inserting != null) Inserting(this, new InsertEventArgs(transaction));
        }

        public event EventHandler<InsertEventArgs> Inserted;
        protected virtual void OnInserted(DbTransaction transaction)
        {
            if (Inserted != null) Inserted(this, new InsertEventArgs(transaction));
        }
        #endregion

        #region Delete
        /// <summary>
        /// Should not call this method directly. It is called in the Persist method,
        /// as appropriate, as a result of calling Save().
        /// </summary>
        /// <param name="cn"></param>
		protected abstract void DeleteEntity(DbTransaction cn);
        /// <summary>
        /// Called before this entity is deleted.
        /// </summary>
        /// <param name="cn"></param>
        public event EventHandler<DeleteEventArgs> Deleting;
        /// <summary>
        /// Called before this entity is deleted.
        /// </summary>
        /// <param name="cn"></param>
        protected virtual void OnDeleting(DbTransaction transaction)
        {
            if (Deleting != null) Deleting(this, new DeleteEventArgs(transaction));
        }
        /// <summary>
        /// Handle this event in custom code to specify which deletes should be cascaded
        /// across deletes.
        /// </summary>
        public event EventHandler<DeleteEventArgs> Deleted;
        /// <summary>
        /// Calls DeleteRelationships.
        /// </summary>
        protected virtual void OnDeleted(DbTransaction transaction)
        {
            if (Deleted != null) Deleted(this, new DeleteEventArgs(transaction));
        }
        /// <summary>
        /// Flags this item as deleted. Will be saved when it is saved next.
        /// </summary>
        public virtual void Delete() { Delete(false); }
        /// <summary>
        /// Flags this item as deleted. If Immediate is true Save() is called also.
        /// </summary>
        /// <param name="Immediate"></param>
        public virtual void Delete(bool immediate)
        {
            _isDeleted = true;
            if (immediate) Save();
        }
        #endregion

        #region Update
        /// <summary>
        /// Cannot call Update directly, update can only be done during a call to Save().
        /// </summary>
        /// <param name="cn"></param>
        protected abstract void UpdateEntity(DbTransaction transaction);

        public event EventHandler<UpdateEventArgs> Updating;
        protected virtual void OnUpdating(DbTransaction transaction)
        {
            if (Updating != null) Updating(this, new UpdateEventArgs(transaction));
        }

        public event EventHandler<UpdateEventArgs> Updated;
        protected virtual void OnUpdated(DbTransaction transaction)
        {
            if (Updated != null) Updated(this, new UpdateEventArgs(transaction));
        }
        #endregion

        #region Fetch
        /// <summary>
        /// 
        /// </summary>
        /// <param name="cn"></param>
        /// <param name="criteria"></param>
        protected virtual void Fetch(DbConnection cn, CriteriaBase criteria)
        {
            if (!CanFetch()) throw new SecurityException(Resources.NotAuthorizedToFetchMessage);
            if (!OnFetching(cn, criteria))
			{
                FetchEntity(cn, criteria);
                OnFetched(cn, criteria);
			}            
        }
        /// <summary>
        /// Should only call this directly in constructors. This stores the code needed
        /// to Fetch this entity. Fetching relationships is left to the developer, at least lazy loading
        /// your relationships is usually reccomended.
        /// </summary>
        /// <param name="cn"></param>
        /// <param name="Parameters"></param>
        protected abstract void FetchEntity(DbConnection cn, CriteriaBase criteria);
		/// <summary>
		/// Implment this event to handle pre fetch conditions. Setting the argument Cancel property
		/// to true will prevent the fetch from occurring and the Fetched event will not fire.
		/// </summary>
		public event EventHandler<FetchEventArgs> Fetching;
		protected virtual bool OnFetching(DbConnection cn, CriteriaBase criteria)
		{
            bool cancel = false;
			if (Fetching != null)
			{
                FetchEventArgs fetchArgs = new FetchEventArgs(cn, criteria);
				Fetching(this, fetchArgs);
                cancel = fetchArgs.Cancel;
			}
			return cancel;
		}
        /// <summary>
        /// Implement this event to add extra behavior during a fetch of this Entity on the same
        /// connection.
        /// </summary>
        public event EventHandler<FetchEventArgs> Fetched;
        protected virtual void OnFetched(DbConnection cn, CriteriaBase criteria)
        {
            if (Fetched != null) Fetched(this, new FetchEventArgs(cn, criteria));
        }
        #endregion

        #region Authorization Rules
        private static AuthorizationRuleCollection _authorizationRules = new AuthorizationRuleCollection();
        /// <summary>
        /// This is a list of Authorization rules related to this Entity and
        /// all of the Roles the current users is a memeber of.
        /// </summary>
        public static AuthorizationRuleCollection AuthorizationRules
        {
            get
            {
                return _authorizationRules;
            }
        }
        /// <summary>
        /// Check to see if this user can Fetch this object.
        /// </summary>
        /// <returns></returns>
        public virtual bool CanFetch()
        {
            OnAuthorizing();
            bool canFetch = EntityBase.CanFetchEntity(this.GetType());
            OnAuthorized();
            return canFetch;
        }

        public static bool CanFetchEntity(Type entityType)
        {
            return AuthorizationRules.CanFetch(entityType, Thread.CurrentPrincipal);
        }
        /// <summary>
        /// Check to see if this user can Insert this object.
        /// </summary>
        /// <returns></returns>
        public virtual bool CanInsert()
        {
            OnAuthorizing();
            bool canInsert = EntityBase.CanInsertEntity(this.GetType());
            OnAuthorized();
            return canInsert;
        }

        public static bool CanInsertEntity(Type entityType)
        {
            return AuthorizationRules.CanInsert(entityType, Thread.CurrentPrincipal);
        }
        /// <summary>
        /// Check to see if this user can Update this object.
        /// </summary>
        /// <returns></returns>
        public virtual bool CanUpdate()
        {
            OnAuthorizing();
            bool canUpdate = EntityBase.CanUpdateEntity(this.GetType());
            OnAuthorized();
            return canUpdate;
        }

        public static bool CanUpdateEntity(Type entityType)
        {
            return AuthorizationRules.CanUpdate(entityType, Thread.CurrentPrincipal);
        }
        /// <summary>
        /// Check to see if this user can Delete this object.
        /// </summary>
        /// <returns></returns>
        public virtual bool CanDelete()
        {
            OnAuthorizing();
            bool canDelete = EntityBase.CanDeleteEntity(this.GetType());
            OnAuthorized();
            return canDelete;
        }

        public static bool CanDeleteEntity(Type entityType)
        {
            return AuthorizationRules.CanDelete(entityType, Thread.CurrentPrincipal);
        }

        public event EventHandler Authorizing;
        protected virtual void OnAuthorizing()
        {
            if (Authorizing != null) Authorizing(this, EventArgs.Empty);
        }

        public event EventHandler Authorized;
        protected virtual void OnAuthorized()
        {
            if (Authorized != null) Authorized(this, EventArgs.Empty);
        }
        #endregion

        #region Access Rules
        private static AccessRuleCollection _AccessRules = new AccessRuleCollection();
        /// <summary>
        /// This is a list of Access rules related to this Entity. These rules specify which roles can
        /// get/set values from various fields.
        /// </summary>
        public static AccessRuleCollection AccessRules
        {
            get { return _AccessRules; }
        }
        /// <summary>
        /// Check to see if this user can Get this field.
        /// </summary>
        /// <returns></returns>
        public virtual void VerifyCanGet(string field)
        {
            OnAccessing();
            if (!EntityBase.AccessRules.CanGetField(this.GetType(), field, Thread.CurrentPrincipal)) 
                throw new SecurityException(
                    string.Format(CultureInfo.InvariantCulture, Resources.SetFieldExceptionMessage, field));
            OnAccessed();
        }
        /// <summary>
        /// Check to see if this user can set this field.
        /// </summary>
        /// <returns></returns>
        public virtual void VerifyCanSet(string field)
        {
            OnAccessing();
            if (!EntityBase.AccessRules.CanSetField(this.GetType(), field, Thread.CurrentPrincipal)) 
                throw new SecurityException(string.Format(CultureInfo.InvariantCulture, Resources.SetFieldExceptionMessage, field));
            OnAccessed();
        }

        public event EventHandler Accessing;
        protected virtual void OnAccessing()
        {
            if (Accessing != null) Accessing(this, EventArgs.Empty);
        }

        public event EventHandler Accessed;
        protected virtual void OnAccessed()
        {
            if(Accessing != null) Accessed(this, EventArgs.Empty);
        }
        #endregion

        #region Validation Rules / IsValid
        /// <summary>
        /// Validates the user properties of this obejct to make sure they are ok to 
        /// be saved. If any of the broken rules are of type ValidationRuleType.Error then this
        /// property will return false.
        /// </summary>
        [Bindable(false)]
        public bool IsValid
        {
            get
            {
                //Clears out existing broken rules and re-validates them.
                //if no rules are broken for this field then this key will not
                //exist in the Broken rules collection.
                lock(_brokenRules)
                    _brokenRules.Clear();

                OnValidating();
                ValidateFields();
                OnValidated();

                bool valid = true;
                foreach (ValidationRule rule in BrokenRules)
                {
                    if (rule.Type == ValidationRuleType.Error)
                    {
                        valid = false;
                        break;
                    }
                }

                return valid;
            }
        }
        private static Collection<ValidationRule> _validationRules = new Collection<ValidationRule>();
        public static Collection<ValidationRule> ValidationRules
        {
            get { return _validationRules; }
        }
        private Collection<ValidationRule> _brokenRules = new Collection<ValidationRule>();
        /// <summary>
        /// A list of all known broken rules.
        /// </summary>
        public Collection<ValidationRule> BrokenRules
        {
            get { return _brokenRules; }
        }
        /// <summary>
        /// Call this method to validate the value of a given field against the provided Validation Rules.
        /// </summary>
        /// <param name="Field">The name of the field to validate.</param>
        /// <param name="Value">The value of the field to validate.</param>
        /// <returns>True if the value is valid.</returns>
        /// <example>ValidateField("SsnNumber", this.SsnNumber);</example>
        protected bool ValidateField(string field, object value)
        {
            bool valid = true;
            foreach (ValidationRule rule in EntityBase.ValidationRules)
            {
                if (rule.EntityType.Equals(this.GetType()) && rule.Field==field && !rule.Validate(value))
                {
                    valid = false;
                    lock(_brokenRules)
                        _brokenRules.Add(rule);
                }
            }
            return valid;
        }
        /// <summary>
        /// Implement this method to validate all fields. If any rules are broken they will be added
        /// to the BrokenRules collection.
        /// </summary>
        /// <example>
        /// protected override void Fields_Validate()
        /// {
        ///     ValidateField("SsnNumber", this.SsnNumber);
        ///     ValidateField("PhoneNumber", this.PhoneNumber);
        ///     ValidateField("Data", this.Data);
        /// }
        /// </example>
        protected abstract void ValidateFields();

        public event EventHandler Validating;
        protected virtual void OnValidating()
        {
            if (Validating != null) Validating(this, EventArgs.Empty);
        }

        public event EventHandler Validated;
        protected virtual void OnValidated()
        {
            if (Validated != null) Validated(this, EventArgs.Empty);
        }
        #endregion

        #region Load
        public virtual void Load(object sender, LoadEventArgs e)
        {
            OnLoading(e);
            LoadEntity(this, e);
            MarkOld();
            OnLoaded(e);
        }

        protected abstract void LoadEntity(object sender, LoadEventArgs e);

        public event EventHandler<LoadEventArgs> Loading;
        protected virtual void OnLoading(LoadEventArgs e)
        {
            if (Loading != null) Loading(this, e);
        }

        public event EventHandler<LoadEventArgs> Loaded;
        protected virtual void OnLoaded(LoadEventArgs e)
        {
            if (Loaded != null) Loaded(this, e);
        }

        #endregion

        #region Actions
        /// <summary>
        /// Attach handlers to events of this entity here.
        /// </summary>
        protected abstract void InitializeActions();
        /// <summary>
        /// This event is for attaching actions without the 'When' specified. It will force the C#
        /// compiler to insure that the attached method has been defined.
        /// </summary>
        protected event EventHandler Never;
        #endregion

        #region Error
        public event EventHandler<EventArgs<Exception>> Error;
        protected virtual void OnError(Exception ex)
        {
            if (Error != null) Error(this, new EventArgs<Exception>(ex));
        }
        #endregion

        #region GetChildTypes
        public abstract Type[] GetChildTypes(); 
        #endregion

    }
}
